import {
    default as request,
    createRequest,
    sendFormRequest
} from 'vue-upload-component/src/utils/request'

export default class ChunkUploadHandler {
    /**
     * Constructor
     *
     * @param {File} file
     * @param {Object} options
     */
    constructor(file, options) {
        this.file = file
        this.options = options
    }

    /**
     * Gets the max retries from options
     */
    get maxRetries() {
        return parseInt(this.options.maxRetries)
    }

    /**
     * Gets the max number of active chunks being uploaded at once from options
     */
    get maxActiveChunks() {
        return parseInt(this.options.maxActive)
    }

    /**
     * Gets the file type
     */
    get fileType() {
        return this.file.type
    }

    /**
     * Gets the file size
     */
    get fileSize() {
        return this.file.size
    }

    /**
     * Gets action (url) to upload the file
     */
    get action() {
        return this.options.action || null
    }

    /**
     * Gets the body to be merged when sending the request in start phase
     */
    get startBody() {
        return this.options.startBody || {}
    }

    /**
     * Gets the body to be merged when sending the request in upload phase
     */
    get uploadBody() {
        return this.options.uploadBody || {}
    }

    /**
     * Gets the body to be merged when sending the request in finish phase
     */
    get finishBody() {
        return Object.assign({
            fileName: this.file.name,
            chunks: this.chunks.length,
            size: this.file.size
        }, this.options.finishBody || {});
    }

    /**
     * Gets the headers of the requests from options
     */
    get headers() {
        return this.options.headers || {}
    }

    /**
     * Whether it's ready to upload files or not
     */
    get readyToUpload() {
        return !!this.chunks
    }

    /**
     * Gets the progress of the chunk upload
     * - Gets all the completed chunks
     * - Gets the progress of all the chunks that are being uploaded
     */
    get progress() {
        const completedProgress = (this.chunksUploaded.length / this.chunks.length) * 100
        const uploadingProgress = this.chunksUploading.reduce((progress, chunk) => {
            return progress + ((chunk.progress | 0) / this.chunks.length)
        }, 0)

        return Math.min(completedProgress + uploadingProgress, 100)
    }

    /**
     * Gets all the chunks that are pending to be uploaded
     */
    get chunksToUpload() {
        return this.chunks.filter(chunk => {
            return !chunk.active && !chunk.uploaded
        })
    }

    /**
     * Whether there are chunks to upload or not
     */
    get hasChunksToUpload() {
        return this.chunksToUpload.length > 0
    }

    /**
     * Gets all the chunks that are uploading
     */
    get chunksUploading() {
        return this.chunks.filter(chunk => {
            return !!chunk.xhr && !!chunk.active
        })
    }

    /**
     * Gets all the chunks that have finished uploading
     */
    get chunksUploaded() {
        return this.chunks.filter(chunk => {
            return !!chunk.uploaded
        })
    }

    /**
     * Creates all the chunks in the initial state
     */
    createChunks() {
        this.chunks = []

        let start = 0
        let end = this.chunkSize
        while (start < this.fileSize) {
            this.chunks.push({
                blob: this.file.file.slice(start, end),
                startOffset: start,
                active: false,
                retries: this.maxRetries
            })
            start = end
            end = start + this.chunkSize
        }
    }

    /**
     * Updates the progress of the file with the handler's progress
     */
    updateFileProgress() {
        this.file.progress = this.progress
    }

    /**
     * Paues the upload process
     * - Stops all active requests
     * - Sets the file not active
     */
    pause() {
        this.file.active = false
        this.stopChunks()
    }

    /**
     * Stops all the current chunks
     */
    stopChunks() {
        this.chunksUploading.forEach(chunk => {
            chunk.xhr.abort()
            chunk.active = false
        })
    }

    /**
     * Resumes the file upload
     * - Sets the file active
     * - Starts the following chunks
     */
    resume() {
        this.file.active = true
        this.startChunking()
    }

    /**
     * Starts the file upload
     *
     * @returns Promise
     * - resolve  The file was uploaded
     * - reject   The file upload failed
     */
    upload() {
        this.promise = new Promise((resolve, reject) => {
            this.resolve = resolve
            this.reject = reject
        })
        this.start()

        return this.promise
    }

    /**
     * Start phase
     * Sends a request to the backend to initialise the chunks
     */
    start() {
        let xhr = createRequest({
            method: 'POST',
            headers: Object.assign({}, this.headers),
            url: this.action
        });

        sendFormRequest(xhr, Object.assign(this.startBody, {
            phase: 'start',
            mime_type: this.fileType,
            size: this.fileSize
        })).then(res => {
            if (res.status !== 'success') {
                this.file.response = res
                return this.reject('server')
            }

            this.sessionId = res.data.session_id
            this.chunkSize = res.data.end_offset

            this.createChunks()
            this.startChunking()
        }).catch(res => {
            this.file.response = res
            this.reject('server')
        })
    }

    /**
     * Starts to upload chunks
     */
    startChunking() {
        for (let i = 0; i < this.maxActiveChunks; i++) {
            this.uploadNextChunk()
        }
    }

    /**
     * Uploads the next chunk
     * - Won't do anything if the process is paused
     * - Will start finish phase if there are no more chunks to upload
     */
    uploadNextChunk() {
        if (this.file.active) {
            if (this.hasChunksToUpload) {
                return this.uploadChunk(this.chunksToUpload[0])
            }

            if (this.chunksUploading.length === 0) {
                return this.finish()
            }
        }
    }

    /**
     * Uploads a chunk
     * - Sends the chunk to the backend
     * - Sets the chunk as uploaded if everything went well
     * - Decreases the number of retries if anything went wrong
     * - Fails if there are no more retries
     *
     * @param {Object} chunk
     */
    uploadChunk(chunk) {
        chunk.progress = 0
        chunk.active = true
        this.updateFileProgress()
        chunk.xhr = createRequest({
            method: 'POST',
            headers: this.headers,
            url: this.action
        })

        chunk.xhr.upload.addEventListener('progress', function (evt) {
            if (evt.lengthComputable) {
                chunk.progress = Math.round(evt.loaded / evt.total * 100)
            }
        }, false)

        sendFormRequest(chunk.xhr, Object.assign(this.uploadBody, {
            phase: 'upload',
            session_id: this.sessionId,
            start_offset: chunk.startOffset,
            chunk: chunk.blob
        })).then(res => {
            chunk.active = false
            if (res.status === 'success') {
                chunk.uploaded = true
            } else {
                if (chunk.retries-- <= 0) {
                    this.stopChunks()
                    return this.reject('upload')
                }
            }

            this.uploadNextChunk()
        }).catch(() => {
            chunk.active = false
            if (chunk.retries-- <= 0) {
                this.stopChunks()
                return this.reject('upload')
            }

            this.uploadNextChunk()
        })
    }

    /**
     * Finish phase
     * Sends a request to the backend to finish the process
     */
    finish() {
        this.updateFileProgress()

        let xhr = createRequest({
            method: 'POST',
            headers: Object.assign({}, this.headers),
            url: this.action
        });

        sendFormRequest(xhr, Object.assign(this.finishBody, {
            phase: 'finish',
            session_id: this.sessionId
        })).then(res => {
            this.file.response = res
            if (res.status !== 'success') {
                return this.reject('server')
            }

            this.resolve(res)
        }).catch(res => {
            this.file.response = res
            this.reject('server')
        })
    }
}
